// 版权归Go作者所有。版权所有。
// 此源代码的使用受BSD样式的约束
// 可以在许可证文件中找到的许可证。

package modfetch

import (
	"bytes"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"io/fs"
	"math/rand"
	"os"
	"path/filepath"
	"strconv"
	"strings"
	"sync"

	"cmd/go/internal/base"
	"cmd/go/internal/cfg"
	"cmd/go/internal/lockedfile"
	"cmd/go/internal/modfetch/codehost"
	"cmd/go/internal/par"
	"cmd/go/internal/robustio"

	"golang.org/x/mod/module"
	"golang.org/x/mod/semver"
)

func cacheDir(path string) (string, error) {
	if err := checkCacheDir(); err != nil {
		return "", err
	}
	enc, err := module.EscapePath(path)
	if err != nil {
		return "", err
	}
	return filepath.Join(cfg.GOMODCACHE, "cache/download", enc, "/@v"), nil
}

func CachePath(m module.Version, suffix string) (string, error) {
	dir, err := cacheDir(m.Path)
	if err != nil {
		return "", err
	}
	if !semver.IsValid(m.Version) {
		return "", fmt.Errorf("non-semver module version %q", m.Version)
	}
	if module.CanonicalVersion(m.Version) != m.Version {
		return "", fmt.Errorf("non-canonical module version %q", m.Version)
	}
	encVer, err := module.EscapeVersion(m.Version)
	if err != nil {
		return "", err
	}
	return filepath.Join(dir, encVer+"."+suffix), nil
}

// DownloadDir返回m应该下载到的目录。
// 如果无法转义模块路径或版本，将返回错误。
// 将返回满足errors.Is（err，fs.ErrNotExist）的错误
// 如果目录不存在，或者如果
// 没有完全填充。
func DownloadDir(m module.Version) (string, error) {
	if err := checkCacheDir(); err != nil {
		return "", err
	}
	enc, err := module.EscapePath(m.Path)
	if err != nil {
		return "", err
	}
	if !semver.IsValid(m.Version) {
		return "", fmt.Errorf("non-semver module version %q", m.Version)
	}
	if module.CanonicalVersion(m.Version) != m.Version {
		return "", fmt.Errorf("non-canonical module version %q", m.Version)
	}
	encVer, err := module.EscapeVersion(m.Version)
	if err != nil {
		return "", err
	}

	// 检查目录本身是否存在。
	dir := filepath.Join(cfg.GOMODCACHE, enc+"@"+encVer)
	if fi, err := os.Stat(dir); os.IsNotExist(err) {
		return dir, err
	} else if err != nil {
		return dir, &DownloadDirPartialError{dir, err}
	} else if !fi.IsDir() {
		return dir, &DownloadDirPartialError{dir, errors.New("not a directory")}
	}

	// 检查.partial文件是否存在。这是在项目开始时创建的
	// 下载并在解压缩后删除。
	partialPath, err := CachePath(m, "partial")
	if err != nil {
		return dir, err
	}
	if _, err := os.Stat(partialPath); err == nil {
		return dir, &DownloadDirPartialError{dir, errors.New("not completely extracted")}
	} else if !os.IsNotExist(err) {
		return dir, err
	}

	// 检查是否存在.ziphash文件。它应该在
	// zip被提取，但如果它被（另一个程序？）删除，我们需要
	// 重新计算。请注意，checkMod将重新填充ziphash
	// 如果该模块不存在，但被检查排除，则返回文件
	// 通过GONOSUMDB或GOPRIVATE，检查和重新填充将不会发生。
	ziphashPath, err := CachePath(m, "ziphash")
	if err != nil {
		return dir, err
	}
	if _, err := os.Stat(ziphashPath); os.IsNotExist(err) {
		return dir, &DownloadDirPartialError{dir, errors.New("ziphash file is missing")}
	} else if err != nil {
		return dir, err
	}
	return dir, nil
}

// 如果是模块目录，则DownloadDir将返回DownloadDirPartialError
// 存在，但未完全填充。
// None
// DownloadDirPartialError相当于fs.ErrNotExist。
type DownloadDirPartialError struct {
	Dir string
	Err error
}

func (e *DownloadDirPartialError) Error() string     { return fmt.Sprintf("%s: %v", e.Dir, e.Err) }
func (e *DownloadDirPartialError) Is(err error) bool { return err == fs.ErrNotExist }

// lockVersion锁定模块缓存中保护下载的文件
// 以及提取给定模块版本的zipfile。
func lockVersion(mod module.Version) (unlock func(), err error) {
	path, err := CachePath(mod, "lock")
	if err != nil {
		return nil, err
	}
	if err := os.MkdirAll(filepath.Dir(path), 0777); err != nil {
		return nil, err
	}
	return lockedfile.MutexAt(path).Lock()
}

// SideLock锁定模块缓存中以前保护的文件
// 编辑缓存外部的文件，例如缓存中的go.sum和go.mod文件
// 用户的工作目录。
// 如果err为nil，则调用方最终必须调用unlock函数。
func SideLock() (unlock func(), err error) {
	if err := checkCacheDir(); err != nil {
		return nil, err
	}

	path := filepath.Join(cfg.GOMODCACHE, "cache", "lock")
	if err := os.MkdirAll(filepath.Dir(path), 0777); err != nil {
		return nil, fmt.Errorf("failed to create cache directory: %w", err)
	}

	return lockedfile.MutexAt(path).Lock()
}

// cachingRepo是围绕基础回购的缓存，
// 避免重复调用ModulePath、Versions、Stat、Latest和GoMod（但不是Zip）。
// 多个goroutine同时使用它也是安全的
// （以便可以多次从查找中返回）。
// 它序列化对基础回购的调用。
type cachingRepo struct {
	path  string
	cache par.Cache // 所有操作的缓存

	once     sync.Once
	initRepo func() (Repo, error)
	r        Repo
}

func newCachingRepo(path string, initRepo func() (Repo, error)) *cachingRepo {
	return &cachingRepo{
		path:     path,
		initRepo: initRepo,
	}
}

func (r *cachingRepo) repo() Repo {
	r.once.Do(func() {
		var err error
		r.r, err = r.initRepo()
		if err != nil {
			r.r = errRepo{r.path, err}
		}
	})
	return r.r
}

func (r *cachingRepo) ModulePath() string {
	return r.path
}

func (r *cachingRepo) Versions(prefix string) ([]string, error) {
	type cached struct {
		list []string
		err  error
	}
	c := r.cache.Do("versions:"+prefix, func() interface{} {
		list, err := r.repo().Versions(prefix)
		return cached{list, err}
	}).(cached)

	if c.err != nil {
		return nil, c.err
	}
	return append([]string(nil), c.list...), nil
}

type cachedInfo struct {
	info *RevInfo
	err  error
}

func (r *cachingRepo) Stat(rev string) (*RevInfo, error) {
	c := r.cache.Do("stat:"+rev, func() interface{} {
		file, info, err := readDiskStat(r.path, rev)
		if err == nil {
			return cachedInfo{info, nil}
		}

		info, err = r.repo().Stat(rev)
		if err == nil {
			// 如果我们将1234abcde解析为v0.0.0-20180604122334-1234abcdef78，
			// 然后将信息保存在正确的版本下，以备将来使用。
			if info.Version != rev {
				file, _ = CachePath(module.Version{Path: r.path, Version: info.Version}, "info")
				r.cache.Do("stat:"+info.Version, func() interface{} {
					return cachedInfo{info, err}
				})
			}

			if err := writeDiskStat(file, info); err != nil {
				fmt.Fprintf(os.Stderr, "go: writing stat cache: %v\n", err)
			}
		}
		return cachedInfo{info, err}
	}).(cachedInfo)

	if c.err != nil {
		return nil, c.err
	}
	info := *c.info
	return &info, nil
}

func (r *cachingRepo) Latest() (*RevInfo, error) {
	c := r.cache.Do("latest:", func() interface{} {
		info, err := r.repo().Latest()

		// 为将来可能的统计呼叫保存信息。
		if err == nil {
			r.cache.Do("stat:"+info.Version, func() interface{} {
				return cachedInfo{info, err}
			})
			if file, _, err := readDiskStat(r.path, info.Version); err != nil {
				writeDiskStat(file, info)
			}
		}

		return cachedInfo{info, err}
	}).(cachedInfo)

	if c.err != nil {
		return nil, c.err
	}
	info := *c.info
	return &info, nil
}

func (r *cachingRepo) GoMod(version string) ([]byte, error) {
	type cached struct {
		text []byte
		err  error
	}
	c := r.cache.Do("gomod:"+version, func() interface{} {
		file, text, err := readDiskGoMod(r.path, version)
		if err == nil {
			// 注意：readDiskGoMod已调用checkGoMod。
			return cached{text, nil}
		}

		text, err = r.repo().GoMod(version)
		if err == nil {
			if err := checkGoMod(r.path, version, text); err != nil {
				return cached{text, err}
			}
			if err := writeDiskGoMod(file, text); err != nil {
				fmt.Fprintf(os.Stderr, "go: writing go.mod cache: %v\n", err)
			}
		}
		return cached{text, err}
	}).(cached)

	if c.err != nil {
		return nil, c.err
	}
	return append([]byte(nil), c.text...), nil
}

func (r *cachingRepo) Zip(dst io.Writer, version string) error {
	return r.repo().Zip(dst, version)
}

// InfoFile类似于Lookup（path）.Stat（version），但返回文件名
// 包含缓存的信息。
func InfoFile(path, version string) (string, error) {
	if !semver.IsValid(version) {
		return "", fmt.Errorf("invalid version %q", version)
	}

	if file, _, err := readDiskStat(path, version); err == nil {
		return file, nil
	}

	err := TryProxies(func(proxy string) error {
		_, err := Lookup(proxy, path).Stat(version)
		return err
	})
	if err != nil {
		return "", err
	}

	// Stat应该已经为我们填充了磁盘缓存。
	file, err := CachePath(module.Version{Path: path, Version: version}, "info")
	if err != nil {
		return "", err
	}
	return file, nil
}

// GoMod类似于查找（path）.GoMod（rev），但避免了
// 查找中的存储库路径解析（如果结果为
// 已缓存在本地磁盘上。
func GoMod(path, rev string) ([]byte, error) {
	// 将提交哈希转换为伪版本
	// 以提高缓存命中率。
	if !semver.IsValid(rev) {
		if _, info, err := readDiskStat(path, rev); err == nil {
			rev = info.Version
		} else {
			if errors.Is(err, statCacheErr) {
				return nil, err
			}
			err := TryProxies(func(proxy string) error {
				info, err := Lookup(proxy, path).Stat(rev)
				if err == nil {
					rev = info.Version
				}
				return err
			})
			if err != nil {
				return nil, err
			}
		}
	}

	_, data, err := readDiskGoMod(path, rev)
	if err == nil {
		return data, nil
	}

	err = TryProxies(func(proxy string) (err error) {
		data, err = Lookup(proxy, path).GoMod(rev)
		return err
	})
	return data, err
}

// GoModFile类似于GoMod，但返回包含
// 缓存的信息。
func GoModFile(path, version string) (string, error) {
	if !semver.IsValid(version) {
		return "", fmt.Errorf("invalid version %q", version)
	}
	if _, err := GoMod(path, version); err != nil {
		return "", err
	}
	// GoMod应该为我们填充磁盘缓存。
	file, err := CachePath(module.Version{Path: path, Version: version}, "mod")
	if err != nil {
		return "", err
	}
	return file, nil
}

// GoModSum返回模块版本的go.mod文件的go.sum条目。
// （也就是说，它将go.sum中列出的条目返回为“path version/go.mod”。）
func GoModSum(path, version string) (string, error) {
	if !semver.IsValid(version) {
		return "", fmt.Errorf("invalid version %q", version)
	}
	data, err := GoMod(path, version)
	if err != nil {
		return "", err
	}
	sum, err := goModSum(data)
	if err != nil {
		return "", err
	}
	return sum, nil
}

var errNotCached = fmt.Errorf("not in cache")

// readDiskStat从磁盘读取缓存的统计结果，
// 返回缓存文件的名称和结果。
// 如果读取失败，调用方可以使用
// writeDiskStat（文件，信息）以写入新的缓存项。
func readDiskStat(path, rev string) (file string, info *RevInfo, err error) {
	file, data, err := readDiskCache(path, rev, "info")
	if err != nil {
		// 如果缓存已经包含具有给定哈希的伪版本，则
		// 之前将返回该伪版本而不检查上游。
		// 然而，这产生了一个不幸的副作用：如果作者添加了
		// 标记到存储库，“go-get”将不会获得新属性的效果
		// 标记现有提交，以及引用这些提交的“go”命令
		// 提交将使用以前的名称而不是新名称。
		// None
		// 如果原始的伪版本以
		// v0.0.0-，与vgo开发期间的所有伪版本一样，
		// 因为v0.0.0伪版本的优先级比任何版本都低
		// 标记版本。
		// None
		// 在实践中，我们只在初始转换过程中通过哈希进行查找
		// 在显式的“go-get”和一点额外的延迟期间
		// 对于这些操作来说，获取更准确的数据似乎是值得的
		// 版本。
		// None
		// 仅当GOPROXY设置禁止时，才返回此解决方案
		// 我们无法解析上游标签。
		if cfg.GOPROXY == "off" {
			if file, info, err := readDiskStatByHash(path, rev); err == nil {
				return file, info, nil
			}
		}
		return file, nil, err
	}
	info = new(RevInfo)
	if err := json.Unmarshal(data, info); err != nil {
		return file, nil, errNotCached
	}
	// 磁盘上可能有已设置名称和短字段的陈旧.info文件。
	// 我们希望规范化为.info文件，并省略这些字段。
	// Remarshal并在需要时更新缓存文件。
	data2, err := json.Marshal(info)
	if err == nil && !bytes.Equal(data2, data) {
		writeDiskCache(file, data)
	}
	return file, info, nil
}

// readDiskStatByHash是readDiskStat在这种情况下的后备方案
// 其中rev是提交哈希，而不是正确的语义版本。
// 在这种情况下，我们将查找匹配的缓存伪版本
// 提交散列。如果我们找到了，我们就用它。
// 这对于转换遗留包管理最为重要
// 配置，当我们经常通过完全散列查找提交时。
// 如果没有此检查，我们将对远程回购进行网络I/O
// 只是为了了解我们已经知道的一项承诺
// （并在其伪版本下缓存）。
func readDiskStatByHash(path, rev string) (file string, info *RevInfo, err error) {
	if cfg.GOMODCACHE == "" {
		// 不要下载到当前目录。
		return "", nil, errNotCached
	}

	if !codehost.AllHex(rev) || len(rev) < 12 {
		return "", nil, errNotCached
	}
	rev = rev[:12]
	cdir, err := cacheDir(path)
	if err != nil {
		return "", nil, errNotCached
	}
	dir, err := os.Open(cdir)
	if err != nil {
		return "", nil, errNotCached
	}
	names, err := dir.Readdirnames(-1)
	dir.Close()
	if err != nil {
		return "", nil, errNotCached
	}

	// 给定的提交哈希可能映射到多个伪版本，
	// 取决于存储库中存在哪些标记。
	// 以最高版本为例。
	var maxVersion string
	suffix := "-" + rev + ".info"
	err = errNotCached
	for _, name := range names {
		if strings.HasSuffix(name, suffix) {
			v := strings.TrimSuffix(name, ".info")
			if module.IsPseudoVersion(v) && semver.Compare(v, maxVersion) > 0 {
				maxVersion = v
				file, info, err = readDiskStat(path, strings.TrimSuffix(name, ".info"))
			}
		}
	}
	return file, info, err
}

// oldVgoPrefix是旧的自动生成的缓存go.mod文件中的前缀。
// 我们停止了自动生成go.mod文件的尝试。现在我们使用一个简单的
// go.mod只包含一个模块行，我们已经删除了版本前缀
// 完全如果我们看到一个版本前缀，这意味着我们正在寻找一个旧的副本
// 我们应该忽略它。
var oldVgoPrefix = []byte("// vgo 0.0。”）

// readDiskGoMod从磁盘读取缓存的go.mod文件，
// 返回缓存文件的名称和结果。
// 如果读取失败，调用方可以使用
// writeDiskGoMod（文件、数据）以写入新的缓存项。
func readDiskGoMod(path, rev string) (file string, data []byte, err error) {
	file, data, err = readDiskCache(path, rev, "mod")

	// 如果文件有一个旧的自动转换前缀，则假装它不在那里。
	if bytes.HasPrefix(data, oldVgoPrefix) {
		err = errNotCached
		data = nil
	}

	if err == nil {
		if err := checkGoMod(path, rev, data); err != nil {
			return "", nil, err
		}
	}

	return file, data, err
}

// readDiskCache是通用的“从缓存文件读取”实现。
// 它采用版本和正在缓存的数据类型的标识后缀。
// 它返回缓存文件的名称和文件的内容。
// 如果读取失败，调用方可以使用
// writeDiskCache（文件、数据）以写入新的缓存项。
func readDiskCache(path, rev, suffix string) (file string, data []byte, err error) {
	file, err = CachePath(module.Version{Path: path, Version: rev}, suffix)
	if err != nil {
		return "", nil, errNotCached
	}
	data, err = robustio.ReadFile(file)
	if err != nil {
		return file, nil, errNotCached
	}
	return file, data, nil
}

// writeDiskStat写入统计结果缓存项。
// 该文件名必须是以前调用readDiskStat时返回的。
func writeDiskStat(file string, info *RevInfo) error {
	if file == "" {
		return nil
	}
	js, err := json.Marshal(info)
	if err != nil {
		return err
	}
	return writeDiskCache(file, js)
}

// writeDiskGoMod写入go.mod缓存项。
// 该文件名必须是以前调用readDiskGoMod时返回的。
func writeDiskGoMod(file string, text []byte) error {
	return writeDiskCache(file, text)
}

// writeDiskCache是通用的“写入缓存文件”实现。
// 该文件必须已由先前对readDiskCache的调用返回。
func writeDiskCache(file string, data []byte) error {
	if file == "" {
		return nil
	}
	// 确保文件的目录存在。
	if err := os.MkdirAll(filepath.Dir(file), 0777); err != nil {
		return err
	}

	// 将文件写入临时位置，然后将其重命名为最终位置
	// 路径，以降低最终路径上存在损坏文件的可能性。
	f, err := tempFile(filepath.Dir(file), filepath.Base(file), 0666)
	if err != nil {
		return err
	}
	defer func() {
		// 只有在未能重命名f.Name（）时才调用os.Remove：否则，
		// 某些其他进程可能在之后创建了同名的新文件
		// 重命名已完成。
		if err != nil {
			f.Close()
			os.Remove(f.Name())
		}
	}()

	if _, err := f.Write(data); err != nil {
		return err
	}
	if err := f.Close(); err != nil {
		return err
	}
	if err := robustio.Rename(f.Name(), file); err != nil {
		return err
	}

	if strings.HasSuffix(file, ".mod") {
		rewriteVersionList(filepath.Dir(file))
	}
	return nil
}

// tempFile使用给定的权限位创建新的临时文件。
func tempFile(dir, prefix string, perm fs.FileMode) (f *os.File, err error) {
	for i := 0; i < 10000; i++ {
		name := filepath.Join(dir, prefix+strconv.Itoa(rand.Intn(1000000000))+".tmp")
		f, err = os.OpenFile(name, os.O_RDWR|os.O_CREATE|os.O_EXCL, perm)
		if os.IsExist(err) {
			continue
		}
		break
	}
	return
}

// rewriteVersionList重写目录中的版本列表
// 写入新的*.mod文件后。
func rewriteVersionList(dir string) (err error) {
	if filepath.Base(dir) != "@v" {
		base.Fatalf("go: internal error: misuse of rewriteVersionList")
	}

	listFile := filepath.Join(dir, "list")

	// 写入listfile时锁定它，以避免损坏该文件。
	// 例如，在极少数情况下，如果系统在
	// 在写入过程中，可能会写入损坏的数据。这是
	// go命令本身没有问题，但如果
	// 缓存由GOPROXY HTTP服务器提供服务。这将得到纠正
	// 下次获取模块的新版本并重写文件时。
	// TODO（matloob）：golang.org/issue/43313包含添加go mod验证
	// 用于删除校验和失败的模块版本的命令。它还应该
	// 删除检测到损坏的列表文件。
	f, err := lockedfile.Edit(listFile)
	if err != nil {
		return err
	}
	defer func() {
		if cerr := f.Close(); cerr != nil && err == nil {
			err = cerr
		}
	}()
	infos, err := os.ReadDir(dir)
	if err != nil {
		return err
	}
	var list []string
	for _, info := range infos {
		// 我们寻找*.mod文件的理论是，如果我们不能提供
		// .mod文件则没有必要列出该版本，
		// 因为它不能用了。（我们可以使用*.info而不使用*.mod。）
		// 我们不需要*.zip文件，因为理论上它只用于代码
		// 参与模块图构造，有很多*.zip文件
		// 永远不会被要求。
		name := info.Name()
		if strings.HasSuffix(name, ".mod") {
			v := strings.TrimSuffix(name, ".mod")
			if v != "" && module.CanonicalVersion(v) == v {
				list = append(list, v)
			}
		}
	}
	semver.Sort(list)

	var buf bytes.Buffer
	for _, v := range list {
		buf.WriteString(v)
		buf.WriteString("\n")
	}
	if fi, err := f.Stat(); err == nil && int(fi.Size()) == buf.Len() {
		old := make([]byte, buf.Len()+1)
		if n, err := f.ReadAt(old, 0); err == io.EOF && n == buf.Len() && bytes.Equal(buf.Bytes(), old) {
			return nil // 不需要编辑。
		}
	}
	// 删除现有内容，这样当我们截断到实际大小时，它将为零填充，
	// 我们将能够检测（一些）不完整的写入，作为包含尾随NUL字节的文件。
	if err := f.Truncate(0); err != nil {
		return err
	}
	// 保留最终尺寸和零填充。
	if err := f.Truncate(int64(buf.Len())); err != nil {
		return err
	}
	// 写出实际内容。如果中途失败，
	// 文件的其余部分应保持为零。
	if _, err := f.Write(buf.Bytes()); err != nil {
		f.Truncate(0)
		return err
	}

	return nil
}

var (
	statCacheOnce sync.Once
	statCacheErr  error
)

// checkCacheDir检查GOMODCACHE指定的目录是否存在。一
// 如果没有，则返回错误。
func checkCacheDir() error {
	if cfg.GOMODCACHE == "" {
		// 如果GOPATH[0]为空，并且cfg.GOMODCACHE为空，则modload.Init退出
		// 如果GOMODCACHE为空，则设置为GOPATH[0]/pkg/mod，因此这种情况永远不会发生。
		return fmt.Errorf("internal error: cfg.GOMODCACHE not set")
	}
	if !filepath.IsAbs(cfg.GOMODCACHE) {
		return fmt.Errorf("GOMODCACHE entry is relative; must be absolute path: %q.\n", cfg.GOMODCACHE)
	}

	// Stat在Windows上运行速度很慢，所以我们只调用它一次，以防止不必要的错误
	// 每次调用此函数时的I/O。
	statCacheOnce.Do(func() {
		fi, err := os.Stat(cfg.GOMODCACHE)
		if err != nil {
			if !os.IsNotExist(err) {
				statCacheErr = fmt.Errorf("could not create module cache: %w", err)
				return
			}
			if err := os.MkdirAll(cfg.GOMODCACHE, 0777); err != nil {
				statCacheErr = fmt.Errorf("could not create module cache: %w", err)
				return
			}
			return
		}
		if !fi.IsDir() {
			statCacheErr = fmt.Errorf("could not create module cache: %q is not a directory", cfg.GOMODCACHE)
			return
		}
	})
	return statCacheErr
}
